#pragma once
#ifdef USE_ESP_IDF

#include "esphome/core/defines.h"
#include <esp_http_server.h>

#include <atomic>
#include <functional>
#include <list>
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>

namespace esphome {
#ifdef USE_WEBSERVER
namespace web_server {
class WebServer;
class ListEntitiesIterator;
};  // namespace web_server
#endif
namespace web_server_idf {

#define F(string_literal) (string_literal)
#define PGM_P const char *
#define strncpy_P strncpy

using String = std::string;

class AsyncWebParameter {
 public:
  AsyncWebParameter(std::string value) : value_(std::move(value)) {}
  const std::string &value() const { return this->value_; }

 protected:
  std::string value_;
};

class AsyncWebServerRequest;

class AsyncWebServerResponse {
 public:
  AsyncWebServerResponse(const AsyncWebServerRequest *req) : req_(req) {}
  virtual ~AsyncWebServerResponse() {}

  // NOLINTNEXTLINE(readability-identifier-naming)
  void addHeader(const char *name, const char *value);

  virtual const char *get_content_data() const = 0;
  virtual size_t get_content_size() const = 0;

 protected:
  const AsyncWebServerRequest *req_;
};

class AsyncWebServerResponseEmpty : public AsyncWebServerResponse {
 public:
  AsyncWebServerResponseEmpty(const AsyncWebServerRequest *req) : AsyncWebServerResponse(req) {}

  const char *get_content_data() const override { return nullptr; };
  size_t get_content_size() const override { return 0; };
};

class AsyncWebServerResponseContent : public AsyncWebServerResponse {
 public:
  AsyncWebServerResponseContent(const AsyncWebServerRequest *req, std::string content)
      : AsyncWebServerResponse(req), content_(std::move(content)) {}

  const char *get_content_data() const override { return this->content_.c_str(); };
  size_t get_content_size() const override { return this->content_.size(); };

 protected:
  std::string content_;
};

class AsyncResponseStream : public AsyncWebServerResponse {
 public:
  AsyncResponseStream(const AsyncWebServerRequest *req) : AsyncWebServerResponse(req) {}

  const char *get_content_data() const override { return this->content_.c_str(); };
  size_t get_content_size() const override { return this->content_.size(); };

  void print(const char *str) { this->content_.append(str); }
  void print(const std::string &str) { this->content_.append(str); }
  void print(float value);
  void printf(const char *fmt, ...) __attribute__((format(printf, 2, 3)));

 protected:
  std::string content_;
};

class AsyncWebServerResponseProgmem : public AsyncWebServerResponse {
 public:
  AsyncWebServerResponseProgmem(const AsyncWebServerRequest *req, const uint8_t *data, const size_t size)
      : AsyncWebServerResponse(req), data_(data), size_(size) {}

  const char *get_content_data() const override { return reinterpret_cast<const char *>(this->data_); };
  size_t get_content_size() const override { return this->size_; };

 protected:
  const uint8_t *data_;
  size_t size_;
};

class AsyncWebServerRequest {
  friend class AsyncWebServer;

 public:
  ~AsyncWebServerRequest();

  http_method method() const { return static_cast<http_method>(this->req_->method); }
  std::string url() const;
  std::string host() const;
  // NOLINTNEXTLINE(readability-identifier-naming)
  size_t contentLength() const { return this->req_->content_len; }

  bool authenticate(const char *username, const char *password) const;
  // NOLINTNEXTLINE(readability-identifier-naming)
  void requestAuthentication(const char *realm = nullptr) const;

  void redirect(const std::string &url);

  void send(AsyncWebServerResponse *response);
  void send(int code, const char *content_type = nullptr, const char *content = nullptr);
  // NOLINTNEXTLINE(readability-identifier-naming)
  AsyncWebServerResponse *beginResponse(int code, const char *content_type) {
    auto *res = new AsyncWebServerResponseEmpty(this);  // NOLINT(cppcoreguidelines-owning-memory)
    this->init_response_(res, code, content_type);
    return res;
  }
  // NOLINTNEXTLINE(readability-identifier-naming)
  AsyncWebServerResponse *beginResponse(int code, const char *content_type, const std::string &content) {
    auto *res = new AsyncWebServerResponseContent(this, content);  // NOLINT(cppcoreguidelines-owning-memory)
    this->init_response_(res, code, content_type);
    return res;
  }
  // NOLINTNEXTLINE(readability-identifier-naming)
  AsyncWebServerResponse *beginResponse(int code, const char *content_type, const uint8_t *data,
                                        const size_t data_size) {
    auto *res = new AsyncWebServerResponseProgmem(this, data, data_size);  // NOLINT(cppcoreguidelines-owning-memory)
    this->init_response_(res, code, content_type);
    return res;
  }
  // NOLINTNEXTLINE(readability-identifier-naming)
  AsyncResponseStream *beginResponseStream(const char *content_type) {
    auto *res = new AsyncResponseStream(this);  // NOLINT(cppcoreguidelines-owning-memory)
    this->init_response_(res, 200, content_type);
    return res;
  }

  // NOLINTNEXTLINE(readability-identifier-naming)
  bool hasParam(const std::string &name) { return this->getParam(name) != nullptr; }
  // NOLINTNEXTLINE(readability-identifier-naming)
  AsyncWebParameter *getParam(const std::string &name);

  // NOLINTNEXTLINE(readability-identifier-naming)
  bool hasArg(const char *name) { return this->hasParam(name); }
  std::string arg(const std::string &name) {
    auto *param = this->getParam(name);
    if (param) {
      return param->value();
    }
    return {};
  }

  operator httpd_req_t *() const { return this->req_; }
  optional<std::string> get_header(const char *name) const;
  // NOLINTNEXTLINE(readability-identifier-naming)
  bool hasHeader(const char *name) const;

 protected:
  httpd_req_t *req_;
  AsyncWebServerResponse *rsp_{};
  std::map<std::string, AsyncWebParameter *> params_;
  std::string post_query_;
  AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}
  AsyncWebServerRequest(httpd_req_t *req, std::string post_query) : req_(req), post_query_(std::move(post_query)) {}
  void init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type);
};

class AsyncWebHandler;

class AsyncWebServer {
 public:
  AsyncWebServer(uint16_t port) : port_(port){};
  ~AsyncWebServer() { this->end(); }

  // NOLINTNEXTLINE(readability-identifier-naming)
  void onNotFound(std::function<void(AsyncWebServerRequest *request)> fn) { on_not_found_ = std::move(fn); }

  void begin();
  void end();

  // NOLINTNEXTLINE(readability-identifier-naming)
  AsyncWebHandler &addHandler(AsyncWebHandler *handler) {
    this->handlers_.push_back(handler);
    return *handler;
  }

 protected:
  uint16_t port_{};
  httpd_handle_t server_{};
  static esp_err_t request_handler(httpd_req_t *r);
  static esp_err_t request_post_handler(httpd_req_t *r);
  esp_err_t request_handler_(AsyncWebServerRequest *request) const;
#ifdef USE_WEBSERVER_OTA
  esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type);
#endif
  std::vector<AsyncWebHandler *> handlers_;
  std::function<void(AsyncWebServerRequest *request)> on_not_found_{};
};

class AsyncWebHandler {
 public:
  virtual ~AsyncWebHandler() {}
  // NOLINTNEXTLINE(readability-identifier-naming)
  virtual bool canHandle(AsyncWebServerRequest *request) const { return false; }
  // NOLINTNEXTLINE(readability-identifier-naming)
  virtual void handleRequest(AsyncWebServerRequest *request) {}
  // NOLINTNEXTLINE(readability-identifier-naming)
  virtual void handleUpload(AsyncWebServerRequest *request, const std::string &filename, size_t index, uint8_t *data,
                            size_t len, bool final) {}
  // NOLINTNEXTLINE(readability-identifier-naming)
  virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {}
  // NOLINTNEXTLINE(readability-identifier-naming)
  virtual bool isRequestHandlerTrivial() const { return true; }
};

#ifdef USE_WEBSERVER
class AsyncEventSource;
class AsyncEventSourceResponse;

using message_generator_t = std::string(esphome::web_server::WebServer *, void *);

/*
  This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
  that will lazily generate that event.  The two pointers allow dedup in the deferred queue if multiple publishes for
  the same component are backed up, and take up only 8 bytes of memory.  The entry in the deferred queue (a
  std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per
  entry (and no heap fragmentation).  Even 100 backed up events (you'd have to have at least 100 sensors publishing
  because of dedup) would take up only 0.8 kB.
*/
struct DeferredEvent {
  friend class AsyncEventSourceResponse;

 protected:
  void *source_;
  message_generator_t *message_generator_;

 public:
  DeferredEvent(void *source, message_generator_t *message_generator)
      : source_(source), message_generator_(message_generator) {}
  bool operator==(const DeferredEvent &test) const {
    return (source_ == test.source_ && message_generator_ == test.message_generator_);
  }
} __attribute__((packed));

class AsyncEventSourceResponse {
  friend class AsyncEventSource;

 public:
  bool try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
  void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
  void loop();

 protected:
  AsyncEventSourceResponse(const AsyncWebServerRequest *request, esphome::web_server_idf::AsyncEventSource *server,
                           esphome::web_server::WebServer *ws);

  void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator);
  void process_deferred_queue_();
  void process_buffer_();

  static void destroy(void *p);
  AsyncEventSource *server_;
  httpd_handle_t hd_{};
  std::atomic<int> fd_{};
  std::vector<DeferredEvent> deferred_queue_;
  esphome::web_server::WebServer *web_server_;
  std::unique_ptr<esphome::web_server::ListEntitiesIterator> entities_iterator_;
  std::string event_buffer_{""};
  size_t event_bytes_sent_;
};

using AsyncEventSourceClient = AsyncEventSourceResponse;

class AsyncEventSource : public AsyncWebHandler {
  friend class AsyncEventSourceResponse;
  using connect_handler_t = std::function<void(AsyncEventSourceClient *)>;

 public:
  AsyncEventSource(std::string url, esphome::web_server::WebServer *ws) : url_(std::move(url)), web_server_(ws) {}
  ~AsyncEventSource() override;

  // NOLINTNEXTLINE(readability-identifier-naming)
  bool canHandle(AsyncWebServerRequest *request) const override {
    return request->method() == HTTP_GET && request->url() == this->url_;
  }
  // NOLINTNEXTLINE(readability-identifier-naming)
  void handleRequest(AsyncWebServerRequest *request) override;
  // NOLINTNEXTLINE(readability-identifier-naming)
  void onConnect(connect_handler_t cb) { this->on_connect_ = std::move(cb); }

  void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
  void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
  void loop();
  bool empty() { return this->count() == 0; }

  size_t count() const { return this->sessions_.size(); }

 protected:
  std::string url_;
  std::set<AsyncEventSourceResponse *> sessions_;
  connect_handler_t on_connect_{};
  esphome::web_server::WebServer *web_server_;
};
#endif  // USE_WEBSERVER

class DefaultHeaders {
  friend class AsyncWebServerRequest;
#ifdef USE_WEBSERVER
  friend class AsyncEventSourceResponse;
#endif

 public:
  // NOLINTNEXTLINE(readability-identifier-naming)
  void addHeader(const char *name, const char *value) { this->headers_.emplace_back(name, value); }

  // NOLINTNEXTLINE(readability-identifier-naming)
  static DefaultHeaders &Instance();

 protected:
  std::vector<std::pair<std::string, std::string>> headers_;
};

}  // namespace web_server_idf
}  // namespace esphome

using namespace esphome::web_server_idf;  // NOLINT(google-global-names-in-headers)

#endif  // !defined(USE_ESP_IDF)
